﻿/// <reference path="AutoCompleteEntry.js" />
/// <reference path="AutoCompleteEntries.js" />
/// <reference path="../../utilities/Xpcom.js" />
///--------------------------------------------------------------------
/// <file name="AutoComplete.js">
///   Encapsulates the view for the AutoComplete feature.
/// </file>
///--------------------------------------------------------------------

NAMFox.ns(function() { with (NAMFox) {

this.GestureType = {
    /// <summary>Indicates a mouse click gesture.</summary>
    MouseClick: 0,

    /// <summary>Indicates that an alphanumeric character was pressed.</summary>
    Character: 1,
    
    /// <summary>Indicates that an up arrow was pressed.</summary>
    ArrowUp: 2,
    
    /// <summary>Indicates that a down arrow was pressed.</summary>
    ArrowDown: 3,
    
    /// <summary>Indicates that the page up key was pressed.</summary>
    PageUp: 4,
    
    /// <summary>Indicates that the page down key was pressed.</summary>
    PageDown: 5
};

this.AutoCompleteItemState = {
    /// <summary>Indicates that no AutoCompleteEntry is selected or suggested.</summary>
    None: 0,
    
    /// <summary>Indicates that an AutoCompleteEntry must be or is selected.</summary>
    Select: 1,
    
    /// <summary>Indicates that an AutoCompleteEntry must be or is suggested.</summary>
    Suggest: 2
};

this.AutoCompleteMode = {
    /// <summary>The default mode.</summary>
    Default: 0,
    
    /// <summary>The mode which supports surrounding text with markup.</summary> 
    Surround: 1
};

this.AutoComplete = function(entries, htmlDocument) {
    /// <summary>
    ///   Initializes a new instance of the AutoComplete class,
    ///   targeted toward the specified document.
    /// </summary>
    /// <param name="entries" type="Array" arrayType="AutoCompleteEntry">
    /// An array of AutoCompleteEntry objects to populate in the UI.
    /// </param>
    /// <param name="htmlDocument" type="Document">
    ///   The document this AutoComplete will be added to.
    /// </param>
    /// <field name="itemState" type="AutoCompleteItemState">
    ///   The current mode or state of the AutoComplete window.
    /// </field>
    /// <field name="isDisplayed" type="Boolean">
    ///   Specifies whether this AutoComplete window is shown.
    /// </field>
    /// <field name="itemChosen" type="Function">
    ///   A callback function used to handle when the user
    ///   makes a choice on what item to use.
    /// </field>
    /// <field name="selectedIndex" type="Number" integer="true">
    ///   The index of the currently selected AutoCompleteEntry.
    /// </field>
    /// <field name="selectedIndexChanged" type="Function">
    ///   A callback function used to handle when the
    ///   selected item has changed.
    /// </field>
    
    // Create a copy of the array so we can correlate AutoCompleteEntries
    // with the UI position of the list item.
    this._entries = entries.slice();
    this._htmlDocument = htmlDocument;
};

AutoComplete.prototype = {
    _autoCompleteHeader: null,
    _autoCompleteList: null,
    _innerContents: null,
    _surroundWith: null,
    _surroundWithContent: null,
    _tooltip: null,
    
    contextReleased: null,
    itemState: AutoCompleteItemState.None,
    isDisplayed: false,
    itemChosen: null,
    mode: AutoCompleteMode.Default,
    selectedEntry: null,
    selectedIndex: -1,
    selectedIndexChanged: null,
    selectedItem: null,
    
    _getEntryHeight: function() {
        /// <summary>
        ///   Calculates the height of a particular list item.
        /// </summary>
        /// <returns type="Number" integer="true" />
        
        var listItem = this._autoCompleteList.childNodes[0];
        
        if (listItem.className === "namfox-auto-complete-selected") {
            // The two pixels added to the height account for
            // the margin or border--the top and bottom parts
            // each account for one pixel.
            return listItem.clientHeight + 2;
        }
        
        return listItem.clientHeight;
    },
    
    _handleGesture: function(targetIndex, action, gestureType) {
        /// <summary>
        ///   Handles an input gesture that selects or suggests
        ///   a particular AutoCompleteEntry list item.
        /// </summary>
        /// <param name="targetIndex" type="Number" integer="true">
        ///   The index of the list item to act on.
        /// </param>
        /// <param name="action" type="AutoCompleteItemState">
        ///   The action to execute on this AutoCompleteEntry.
        /// </param>
        /// <param name="gestureType" type="GestureType">
        ///   The gesture that triggered the action.
        /// </param>
        
        if (!targetIndex || targetIndex < 0) {
            targetIndex = 0;
        }
        
        var entryLength = AutoCompleteEntries.allTags.length;
        if (entryLength <= targetIndex) {
            targetIndex = entryLength - 1;
        }

        // If the indices are the same (no selection change) and the
        // mode of operation is the same, then don't do anything.
        if (this.selectedIndex === targetIndex && this.itemState === action) {
            return;
        }
        
        // First unselect the previous selected item if it
        // doesn't need to be re-selected.
        if (this.selectedIndex !== targetIndex && this.selectedIndex !== -1) {
            AutoCompleteEntry.unselectItem(this.selectedItem);
        }
        
        var entryListItem = this._autoCompleteList.childNodes[targetIndex];
        
        if (action === AutoCompleteItemState.Select) {
            AutoCompleteEntry.selectItem(entryListItem);
        }
        else {
            AutoCompleteEntry.suggestItem(entryListItem);
        }
        
        this.itemState = action;
        
        // Scroll to that item if the event is not caused by a mouse click.
        
        var scrollTop = this._innerContents.attr("scrollTop");
        var entryHeight = this._getEntryHeight();
        var autoCompleteHeight = this._innerContents.attr("clientHeight");
        
        var itemsInView = Math.floor(autoCompleteHeight / entryHeight);
        var itemsAboveView = Math.floor(scrollTop / entryHeight);
        
        var itemOffset = 0;
        switch (gestureType) {
            case GestureType.Character:
                itemOffset = (targetIndex * entryHeight) - (3 * entryHeight);
                break;
                
            case GestureType.MouseClick:
                itemOffset = scrollTop;
                break;
                
            case GestureType.ArrowDown:
                if (targetIndex - itemsAboveView >= itemsInView) {
                    itemOffset = scrollTop + entryHeight;
                }
                else {
                    itemOffset = scrollTop;
                }
                break;
                
            case GestureType.ArrowUp:
                if (itemsAboveView >= targetIndex) {
                    itemOffset = Math.max(0, scrollTop - entryHeight);
                }
                else {
                    itemOffset = scrollTop;
                }
                break;
                
            case GestureType.PageDown:
                // Position at the bottom of the window.
                // The 2 is for the top padding of the UL element.
                itemOffset = (targetIndex - itemsInView) * entryHeight +
                    (entryHeight - ((autoCompleteHeight - 2) % entryHeight));
                break;
                
            case GestureType.PageUp:
                // Position at the top of the window.
                itemOffset = targetIndex * entryHeight;
                break;
        }
        
        if (itemOffset !== scrollTop) {
            // Unfortunately setting the scrollTop property takes
            // a very long time. I hope this will speed up in later version
            // of Fx.
            
            this._innerContents.attr("scrollTop", itemOffset);
        }
        
        this.selectedIndex = targetIndex;
        this.selectedItem = this._autoCompleteList.childNodes[targetIndex];
        this.selectedEntry = this._entries[targetIndex];
        
        if (this.selectedIndexChanged) {
            this.selectedIndexChanged();
        }
        
        this._showTooltip();
    },
    
    _showTooltip: function() {
        /// <summary>
        ///   Shows a tooltip with information about the
        ///   currently selected item.
        /// </summary>
        
        if (this.selectedIndex !== -1) {
            this._tooltip.html(this.selectedEntry.getDescription()).
                css({
                    left: (parseInt(this._autoCompleteHeader.attr("viewportOffsetLeft")) +
                        parseInt(this._autoCompleteHeader.attr("clientWidth")) +
                        5) +
                        "px",
                    top: (parseInt(this._autoCompleteHeader.attr("viewportOffsetTop")) +
                        this.selectedItem.offsetTop -
                        parseInt(this._innerContents.attr("scrollTop")) +
                        3) +
                        "px",
                    visibility: "visible",
                    zIndex: 10000
                });
        }
    },
    
    chooseItem: function(index) {
        /// <summary>
        ///   Instructs the AutoComplete window to force an
        ///   AutoCompleteEntry to be chosen.
        /// </summary>
        /// <param name="textArea" type="TextArea">
        ///   The text area for which to choose the item.
        /// </param>
        /// <param name="index" type="Number" integer="true" optional="true">
        ///   (Optional) The index of the AutoCompleteEntry to choose.
        /// </param>

        if (!index) {
            index = this.selectedIndex;
        }
        
        this.hide(true);
        
        if (this.itemChosen) {
            this.itemChosen(this._entries[index]);
        }
        
        this.releaseContext();
    },
    
    hide: function(suppressReleaseContext) {
        /// <summary>
        ///   Hides the AutoComplete window.
        /// </summary>
        /// <param name="suppressReleaseContext" type="Boolean" optional="true">
        ///   (Optional) Specifies whether to suppress this method
        ///   from raising the contextReleased event.
        /// </param>
        
        if (this.selectedIndex !== -1) {
            AutoCompleteEntry.unselectItem(this._autoCompleteList.childNodes[this.selectedIndex]);
        }
        
        this._autoCompleteHeader.css({
            left: "-250px",
            visibility: "hidden",
            zIndex: -2
        });
        this._surroundWith.css({
            visibility: "hidden",
            zIndex: -2
        });
        this._tooltip.css({
            visibility: "hidden",
            zIndex: -2
        });
        
        this._surroundWithContent.html("");
        
        this.isDisplayed = false;
        this.selectedIndex = -1;
        
        if (!suppressReleaseContext) {
            this.releaseContext();
        }
    },
    
    pageDown: function() {
        /// <summary>
        ///   Handles a page down key press.
        /// </summary>
        
        this.select(
            this.selectedIndex + Math.round(this._innerContents.attr("clientHeight") / this._getEntryHeight()),
            GestureType.PageDown
        );
    },
    
    pageUp: function() {
        /// <summary>
        ///   Handles a page up key press.
        /// </summary>
        
        this.select(
            this.selectedIndex - Math.round(this._innerContents.attr("clientHeight") / this._getEntryHeight()),
            GestureType.PageUp
        );
    },
    
    releaseContext: function() {
        /// <summary>
        ///   Raises the contextReleased event.
        /// </summary>
        
        if (this.contextReleased) {
            this.contextReleased();
        }
    },
    
    remove: function() {
        /// <summary>
        ///   Removes the AutoComplete window from the page.
        /// </summary>
        
        this._autoCompleteHeader.remove();
        this._tooltip.remove();
        this._surroundWith.remove();
    },
    
    render: function() {
        /// <summary>
        ///   Renders the AutoComplete UI element on the page.
        /// </summary>
        
        var body = $FX(this._htmlDocument.body);
        
        var autoCompleteHtml = '';
        autoCompleteHtml += '<div id="namfox-auto-complete">';
        autoCompleteHtml += '  <div id="namfox-auto-complete-outer">';
        autoCompleteHtml += '    <div id="namfox-auto-complete-inner">';
        autoCompleteHtml += '      <ul id="namfox-auto-complete-entries"></ul>';
        autoCompleteHtml += '    </div>';
        autoCompleteHtml += '    <a id="namfox-auto-complete-help-link" target="_blank">Help</a>';
        autoCompleteHtml += '  </div>';
        autoCompleteHtml += '</div>';
        
        var tooltipHtml = '<div id="namfox-auto-complete-tooltip"></div>';
        
        var surroundWithHtml = '';
        surroundWithHtml += '<div id="namfox-auto-complete-surround-with">';
        surroundWithHtml += '  <span id="namfox-auto-complete-surround-with-header">Surround With: </span>';
        surroundWithHtml += '  <span id="namfox-auto-complete-surround-with-content"></span>';
        surroundWithHtml += '</div>';
        
        body.append(autoCompleteHtml);
        body.append(tooltipHtml);
        body.append(surroundWithHtml);

        // Add the AutoCompleteEntry list items to the AutoComplete window.
        this._autoCompleteList = this._htmlDocument.getElementById("namfox-auto-complete-entries");
        var fxAutoCompleteList = $FX(this._autoCompleteList);
        var autoCompleteEntries = this._entries;
        var that = this;

        for (var i = 0; i < autoCompleteEntries.length; ++i) {
            var entry = this._entries[i];
            fxAutoCompleteList.append("<li>" + entry.getTitle() + "</li>");
        }

        $FX(this._autoCompleteList).find("li").each(function(i) {
            var entry = autoCompleteEntries[i];
            this.index = entry.uiPosition = i;
            entry.addAttributes(this);
        }).click(
            function(event) {
                event.stopPropagation();
                that.select(event.target.index, GestureType.MouseClick);
            }
        ).dblclick(
            function(event) {
                event.stopPropagation();
                that.chooseItem(event.target.index);
            }
        );

        // Prevent clicks on the tooltip from hiding AutoComplete.
        this._tooltip = $FX("#namfox-auto-complete-tooltip", this._htmlDocument);
        this._tooltip.click(
            function(event) {
                event.stopPropagation();
            }
        );
        
        this._autoCompleteHeader = $FX("#namfox-auto-complete", this._htmlDocument);
        this._innerContents = $FX("#namfox-auto-complete-inner", this._htmlDocument);
        this._surroundWith = $FX("#namfox-auto-complete-surround-with", this._htmlDocument);
        this._surroundWithContent = $FX("#namfox-auto-complete-surround-with-content", this._htmlDocument);
        
        $FX("#namfox-auto-complete-help-link", this._htmlDocument).attr(
            "href",
            $.url.forum("/115/t1187670-autocomplete-walkthrough/")
        );
    },
    
    select: function(index, gestureType) {
        /// <summary>
        ///   Selects an AutoCompleteEntry in the list.
        /// </summary>
        /// <param name="index" type="Number" integer="true">
        ///   The index of the AutoCompleteEntry to select.
        /// </param>
        /// <param name="gestureType" type="GestureType" optional="true">
        ///   (Optional) Indicates how the AutoComplete window should
        ///   scroll to handle the selection. The default
        ///   is GestureType.KeyPress.
        /// </param>
        
        // Check for undefined or null
        if (gestureType == null) {
            gestureType = GestureType.Character;
        }
        
        this._handleGesture(index, AutoCompleteItemState.Select, gestureType);
    },
    
    show: function(point, mode) {
        /// <summary>
        ///   Shows the Autocomplete window at the specified point.
        /// </summary>
        /// <param name="point">
        ///   Contains x and y properties to indicate where to place the
        ///   AutoComplete window.
        /// </param>
        /// <param name="mode" type="AutoCompleteMode">
        ///   The mode to show AutoComplete in.
        /// </param>
        
        var x = point.x;
        var y = point.y;
        
        if (mode === AutoCompleteMode.Surround) {
            y += 20;
        }
        
        this._autoCompleteHeader.css({
            left: x + "px",
            top: y + "px"
        }).attr({
            viewportOffsetLeft: x,
            viewportOffsetTop: y
        });
        
        if (mode === AutoCompleteMode.Surround) {
            this._surroundWith.css({
                left: x + "px",
                top: (y - 20) + "px",
                visibility: "visible",
                zIndex: 10000
            });
        }
        
        this._showTooltip();
        
        if (this.isDisplayed) {
            return;
        }
        
        // TODO: Is this necessary? I thought we unselected the selected
        // item every time AutoComplete was closed...
        if (this.selectedIndex !== -1) {
            AutoCompleteEntry.unselectItem(this._autoCompleteList.childNodes[this.selectedIndex]);
        }
        
        this._autoCompleteHeader.css({
            visibility: "visible",
            zIndex: 10000
        });
        
        this._innerContents.attr("scrollTop", 0);
        this.isDisplayed = true;
        this.mode = mode;
        this.selectedIndex = -1;
    },
    
    suggest: function(index) {
        /// <summary>
        ///   Suggests an AutoCompleteEntry in the list.
        /// </summary>
        /// <param name="index" type="Number" integer="true">
        ///   The index of the AutoCompleteEntry to select.
        /// </param>
        
        this._handleGesture(index, AutoCompleteItemState.Suggest, GestureType.Character);
    },
    
    updateSurroundWithText: function(text) {
        /// <summary>
        ///   Updates the text displayed in the surround with element.
        /// </summary>
        /// <param name="text" type="String">
        ///   The text to display.
        /// </param>
        
        if (text || text === "") {
            this._surroundWithContent.html(text);
        }
    }
};

}});